package jmine.tec.batch.domain;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;

import jmine.tec.batch.JmineTecBatchMessages;
import jmine.tec.batch.PhaseException;
import jmine.tec.batch.dao.ProcessDAO;
import jmine.tec.persist.Versioned;
import jmine.tec.persist.annotation.Alias;
import jmine.tec.persist.annotation.Comment;
import jmine.tec.persist.annotation.Comments;
import jmine.tec.persist.annotation.Constraint;
import jmine.tec.persist.annotation.DiscriminatorComment;
import jmine.tec.persist.annotation.Index;
import jmine.tec.persist.annotation.Indexes;
import jmine.tec.persist.schema.api.IndexOrder;

import org.hibernate.annotations.ForeignKey;
import org.hibernate.validator.NotNull;

import bancosys.tec.component.Documentation;
import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.bussobj.PersistableBusinessObject;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.persister.annotation.NaturalKey;
import bancosys.tec.persist.persister.annotation.SystemId;
import bancosys.tec.utils.date.Timestamp;
import bancosys.tec.utils.reflection.ReflectionUtils;

// TODO [Frank] Criar constraint unica no mnemonico.
/**
 * Representa a meta informação de um processo.
 * <p>
 * É responsável por instanciar o {@link Process} e as {@link ProcessingUnit} através de um factory method:
 * {@link MProcess#createProcess(PhaseableElementGroup)}. <br>
 * Utiliza o {@link PhaseableElementGroup} que transporta os agrupamentos de elementos.
 * <p>
 * O process criado será do tipo classname, se esse campo estiver preenchido, senão será do tipo base Process.
 * 
 * @author Frank Cara
 * @see Process
 * @see ProcessingUnit
 * @see PhaseableElementGroup
 */
@Entity
@Table(name = "MPROCESS")
@Alias("MPROCE")
@SequenceGenerator(name = "SEQ_MPROCE", sequenceName = "SEQ_MPROCE")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.INTEGER)
@Comment(table = "MPROCESS", column = "DISCRIMINATOR", value = "IDENTIFICADOR DO PROCESSO.")
@Constraint(suffix = "0")
@DiscriminatorValue(value = "0")
@DiscriminatorComment("PROCESSO PADRAO")
@Documentation("ARMAZENA OS PROCESSOS EXECUTADOS PELA CARTEIRA. TABELA PREENCHIDA VIA POST SCRIPT."
        + "NAO HA CADASTRO NO SISTEMA PARA INCLUSAO/ALTERACAO DOS SEUS DADOS.")
public class MProcess extends PersistableBusinessObject {
    private Long id;

    private String mnemonico;

    private String descr;

    private String className;

    private Set<MPhase> phases = new HashSet<MPhase>();

    /**
     * Deve ser inicializada com nulo. Ver o método getPHaseByMnemonico.
     */
    private Map<String, MPhase> phasesByMnemo = null;

    /**
     * @return o valor do id.
     */
    @Id
    @SystemId
    @Column(name = "COD_MPROCESS")
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_MPROCE")
    @Documentation("CODIGO DO PROCESSO.")
    public Long getId() {
        return this.id;
    }

    /**
     * Seta o valor do Id.
     * 
     * @param id o valor do id.
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * O mnemonico do processo.
     * 
     * @return o mnemonico.
     */
    @NotNull
    @Column(name = "MNE", length = 40, nullable = false)
    @Documentation("MNEMONICO DO PROCESSO.")
    @NaturalKey
    public String getMnemonico() {
        return this.mnemonico;
    }

    /**
     * Seta o mnemonico do process.
     * 
     * @param mnemonico o valor do mnemonico.
     */
    public void setMnemonico(String mnemonico) {
        this.mnemonico = mnemonico;
    }

    /**
     * Retorna a descrição do processo.
     * 
     * @return a descrição.
     */
    @Column(name = "DESCR", length = 110)
    @Documentation("DESCRICAO DO PROCESSO.")
    public String getDescr() {
        return this.descr;
    }

    /**
     * Seta a descrição do processo.
     * 
     * @param descr o valor da descrição.
     */
    public void setDescr(String descr) {
        this.descr = descr;
    }

    /**
     * As fases que compoem esse processo.
     * 
     * @return as fases que compoem esse processo.
     */
    @ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH })
    @JoinTable(name = "PROCESS_TO_PHASE", joinColumns = @JoinColumn(name = "COD_MPROCESS"), inverseJoinColumns = @JoinColumn(name = "COD_MPHASE"))
    @Alias("PROTPH")
    @ForeignKey(name = "FK_MPROCE_PROTPH", inverseName = "FK_MPHASE_PROTPH")
    @Comments({
            @Comment(table = "PROCESS_TO_PHASE", value = "ARMAZENA A ASSOCIACAO ENTRE O PROCESSO E AS FASES. TABELA PREENCHIDA VIA POST SCRIPT."
                    + "NAO HA CADASTRO NO SISTEMA PARA INCLUSAO/ALTERACAO DOS SEUS DADOS."),
            @Comment(table = "PROCESS_TO_PHASE", column = "COD_MPHASE", value = "CODIGO DA FASE DO PROCESSO."),
            @Comment(table = "PROCESS_TO_PHASE", column = "COD_MPROCESS", value = "CODIGO DO PROCESSO.") })
    @Indexes({ @Index(columns = "COD_MPROCESS", suffix = "0", orders = IndexOrder.ASC),
            @Index(columns = "COD_MPHASE", suffix = "1", orders = IndexOrder.ASC) })
    public Set<MPhase> getPhases() {
        return this.phases;
    }

    /**
     * Seta as fases que compoem esse processo.
     * 
     * @param phases seta as fases que compoem esse processo.
     */
    public void setPhases(Set<MPhase> phases) {
        this.phases = phases;
    }

    /**
     * Adiciona uma fase as fases que compoem o processo.
     * 
     * @param phase a fase a ser adicionada.
     */
    public void addPhase(MPhase phase) {
        if (this.phases.contains(phase)) {
            return;
        }
        this.phases.add(phase);
    }

    /**
     * Retorna a phase equivalente ao mnemonico passado.
     * 
     * @param mnemo o mnemonico
     * @return a fase, ou nulo.
     */
    @Transient
    public MPhase getPhaseByMnemonico(String mnemo) {
        if (this.phasesByMnemo == null) {
            this.phasesByMnemo = new HashMap<String, MPhase>();
            for (MPhase phase : this.getPhases()) {
                this.phasesByMnemo.put(phase.getMnemonico(), phase);
            }
        }
        return this.phasesByMnemo.get(mnemo);
    }

    /**
     * O nome da classe que extende o {@link Process}. Utilizado para instanciar processos no dispatch do processamento. <br>
     * Pode ser nula!
     * 
     * @return o nome da classe do Process.
     * @see MProcess#createProcess(PhaseableElementGroup)
     */
    @Column(name = "CLASSNAME", length = 110)
    @Documentation("CLASSE A SER INSTANCIADA EM CASO DE EXTENSAO DE MPROCESS.")
    public String getClassName() {
        return this.className;
    }

    /**
     * O nome da classe que extende o {@link Process}. Utilizado para instanciar processos no dispatch do processamento. <br>
     * 
     * @param className o nome da classe do Process.
     * @see MProcess#createProcess(PhaseableElementGroup)
     */
    public void setClassName(String className) {
        this.className = className;
    }

    // TODO [Frank] Substituirá o ProcessoBuilder utilizado pelo ProcessamentoController do BWAM.
    /**
     * Factory de {@link Process} e {@link ProcessingUnit}, utilizado por quem dispara processos do controle de fases.
     * <p>
     * A partir dos agrupamentos dos elementos a serem processados {@link PhaseableElementGroup}, instancia o Process da classe definida
     * pelo classname, ou o tipo Process se classname for NULL.
     * 
     * @param <E> Generics que representa o tipo do elemento.
     * @param <U> Generics que representa o tipo da unidade de processamento.
     * @param group o bean que agrupa os elementos.
     * @return a lista de unidades de processamento criadas.
     * @throws PhaseException em caso de erro.
     */
    public <E extends Persistable & Versioned, U extends ProcessingUnit<E>> List<U> createProcess(PhaseableElementGroup<E, U> group)
            throws PhaseException {
        ProcessDAO dao = this.getController().getDAOFactory().getDAOByClass(ProcessDAO.class);
        Process p = null;
        if (this.getClassName() == null) {
            p = dao.createBean();
        } else {
            p = this.instanciateProcess();
        }
        p.setQtdUnits(group.size());
        p.setStartTime(new Timestamp());
        p.setStatus(Process.NOT_STARETD);
        p.getPurePersister().save();
        return this.createProcessingUnit(p, group);
    }

    /**
     * Utilizado para instanciar um processo extendido de {@link Process}, quando esse dado estiver preenchido no MPROCESS.
     * 
     * @param <Pc> Generics que representa uma extensão de Process.
     * @return o Process instanciado.
     * @throws PhaseException em caso de erro na instanciação.
     */
    @SuppressWarnings("unchecked")
    private <Pc extends Process> Pc instanciateProcess() throws PhaseException {
        Class<Pc> clazz = null;
        try {
            clazz = (Class<Pc>) ReflectionUtils.findClass(this.getClassName(), this.getClass().getClassLoader());
        } catch (Throwable e) {
            throw new PhaseException(JmineTecBatchMessages.PHASE_ERROR_CREATING_PROCESS.create(this.getMnemonico(), this.getClassName()),
                    e);
        }

        BaseDAO<Pc> dao = this.getController().getDAOFactory().getGenericDAO(clazz);

        return dao.createBean();
    }

    /**
     * Cria o conjunto unidades de processamento para o processo.
     * <p>
     * As unidades são criadas a partir do {@link PhaseableElementGroup}, que é um bean que transporta o agrupamento dos elementos. <br>
     * Esse método utiliza o {@link ProcessingUnit#addChildren(Object)} para adicionar os elementos à unidade de processamento. <br>
     * Dessa forma, deve atuar sobre uma extensão concreta do ProcessingUnit.
     * 
     * @param <E> Generics que representa o tipo do elemento.
     * @param <U> Generics que representa o tipo da unidade de processamento.
     * @param process o processo.
     * @param group o bean que agrupa os elementos.
     * @return lista de processing units do tipo P.
     */
    private <E extends Persistable & Versioned, U extends ProcessingUnit<E>> List<U> createProcessingUnit(Process process,
            PhaseableElementGroup<E, U> group) {
        List<U> units = new ArrayList<U>();
        for (Iterator<List<E>> iterator = group.iterator(); iterator.hasNext();) {
            List<E> elements = iterator.next();
            U unit = group.createProcessingUnit();
            unit.setProcess(process);
            unit.setStatus(ProcessingUnit.NOT_STARETD);
            unit.setStartTime(new Timestamp());
            for (E el : elements) {
                unit.addChildren(el);
            }
            unit.getPurePersister().save();
            units.add(unit);
        }
        return units;
    }

    /**
     * @return a string representando a instância, contendo o ID e o MNEMONICO da fase.
     */
    @Override
    public String toString() {
        return "[" + this.getId() + "] " + this.getMnemonico();
    }
}
